﻿
function DoubanSubject() {
	this.link = "";
	this.id = "";
	this.kind = "";
	this.image = "";
	this.title = "";
	this.hot = 1;
	this.published = 0;
	this.comments = new Array();
	this.description = "";
	this.related = new Array();
}

function DoubanBuddyInfo() {
	this.started = false;
	this.itempool = new Object();
	this.hots = new Array();   // keep the final subject in
	this.subjects = new Array();    // keep the temp collected subjects in
	this.sayings = {};  // keep the saying item in
	this.subjectInfo = new Object();
	this.MAX_LOOP = 20;
	this.loop = 0;
	this.status = "";
	this.lastupdate = new Date();
}

DoubanBuddyInfo.prototype = {
	
	load: function() {
		if (!doubanbuddyInfo.started) {
			var jsm = {};
			Components.utils.import("resource://douban-modules/bridge.js", jsm);
			jsm.DoubanBridge.setDoubanBuddy(doubanbuddyInfo);
			doubanbuddyInfo.started = true;

			var accessToken = Application.prefs.getValue("extensions.doubanbuddy.access_token", "");
			var accessTokenSecret = Application.prefs.getValue("extensions.doubanbuddy.access_token_secret", "");
			if (accessToken && accessTokenSecret) {
				DoubanApiInfo.access_token = accessToken;
				DoubanApiInfo.access_token_secret = accessTokenSecret;
				
				// get user id
				setTimeout("doubanbuddyInfo.startAnalysis()", 500);
			} else {
				// get request token and secret
				setTimeout("doubanbuddyInfo.getRequestToken()", 0);
			}
			setTimeout("doubanbuddyInfo.checkVersion()", 500);
		}
	},
	
	unload: function() {
		this.started = false;
	},
	
	getRequestToken: function() {
		doubanbuddyInfo.status = "request";
		DoubanApi.getRequestToken(this.startAuth, this.failRequestToken);
	},
	
	startAuth: function() {
		Application.console.log("doubanbuddy: wait for auth");
		doubanbuddyInfo.status = "auth@" + DoubanApiInfo.authorization_uri + DoubanApiInfo.request_token;
		// just wait for the user clicke the auth link
	},
	
	failRequestToken: function(status) {
		Application.console.log("doubanbuddy: fail to get request token, maybe network is broken or status code = " + status);
		// request the token after 30 mins
		setTimeout("doubanbuddyInfo.getRequestToken()", 30 * 60 * 1000);
	},
	
	completeAuth: function() {
		Application.console.log("doubanbuddy: complete authentication");
		doubanbuddyInfo.status = "access";
		setTimeout("doubanbuddyInfo.getAccessToken()", 500);
	},
	
	getAccessToken: function() {
		DoubanApi.getAccessToken(this.startAccess, this.failAccessToken);
	},
	
	startAccess: function() {
		setTimeout("doubanbuddyInfo.startAnalysis()", 500);
		// set prefs values
		Application.prefs.setValue("extensions.doubanbuddy.access_token", DoubanApiInfo.access_token);
		Application.prefs.setValue("extensions.doubanbuddy.access_token_secret", DoubanApiInfo.access_token_secret);
	}, 
	
	failAccessToken: function(status) {
		Application.console.log("doubanbuddy: fail to get access token, maybe network is broken or status code = " + status);
		// re get the request token and finish the authentication
		setTimeout("doubanbuddyInfo.getRequestToken()", 500);
	},
	
	checkVersion: function() {
		var version = Application.prefs.getValue("extensions.doubanbuddy.version", "");
		var extver = Application.extensions.get("doubanbuddy@mijia.org").version;
		var openDouban = false;
		if (version) {
			try {
				var x = Components.classes["@mozilla.org/xpcom/version-comparator;1"]
    	               .getService(Components.interfaces.nsIVersionComparator)
        	           .compare(version, extver);
            	if (x < 0) {
            		openDouban = true;
            	}
            } catch (e) { openDouban = true; }
		} else {
			openDouban = true;
		}
		Application.prefs.setValue("extensions.doubanbuddy.version", extver);
		if (openDouban) {
			this.openAboutDouban();
		}
	},
	
	startAnalysis: function() {
		this.status = "refresh";
		this.itempool = new Object();
		this.subjects = new Array();
		this.sayings = {};
		this.loop = this.MAX_LOOP;
		DoubanNGram.reset();
		this.crawlData();
	},

	crawlData: function() {
		if (this.loop > 0) {
			Application.console.log("doubanbuddy: crawl loop #" + this.loop);
			this.loop--;
			var startIndex = (this.MAX_LOOP - this.loop - 1) * 50 + 1;
			DoubanApi.getMiniblog(startIndex, this.loadMiniblogData, this.error);
		}
	},
	
	extractSubject: function(entry) {
		var id = $("id", entry).text();
		if (this.itempool[id]) {
			Application.console.log("doubanbuddy: found duplicated id = " + id);
			return;
		}
		this.itempool[id] = true;
		
		var scheme = $("category", entry).attr("scheme");
		if (scheme != "http://www.douban.com/2007#kind")
			return;
		
		var kindTerm = $("category", entry).attr("term").substring("http://www.douban.com/2007#miniblog.".length);
		
		var publishTime = $("published", entry).text();
		var published = 0;
		if (publishTime) {
			published = iso8601ToJSDate(publishTime).getTime() / 1000;
		}
		
		// find subject links
		var content = $("content", entry).text();
		var allpattern = /<a\s+href=["|']?(.*?)["|']?>(.*?)<\/a>/ig;
		var linkpattern = /<a\s+href=["|']?(.*?)["|']?>(.*?)<\/a>/i;
		var links = content.match(allpattern);
		if (!links || links.length < 1 || kindTerm == "saying" || kindTerm == 'photo' ) {
			if (kindTerm == "saying") {
				var sayingid = $("id", entry).text();
				DoubanNGram.extractNGram(sayingid, content);
				var author = $("author > name", entry).text();
				this.sayings[sayingid] = '<span style="' + this.getRandomSpanStyle() + '">[' + author + "]说: " + content.replace(/(<([^>]+)>)/gi, "") + "</span>";
			}
			Application.console.log("doubanbuddy: omit the miniblog without subject links - " + content);		
			return;
		}
		var split = links[0].match(linkpattern);
		
		
		var subject = this.findSubject(split[1], this.subjects);
		if (subject) {
			subject.hot++;
			subject.published = published;
		} else {
			if (kindTerm == "recommendation") {
				$("db\\:attribute", entry).each(function() {
					if ($(this).attr("name") == "category") 
						kindTerm = $(this).text();
				});	
			}
			subject = new DoubanSubject();
			subject.link = split[1];
			subject.kind = kindTerm;
			subject.title = split[2];
			DoubanNGram.extractNGram(subject.link, subject.title);
			subject.published = published;
			var idpattern = /(\d[\d]+)/i;
			var ids = subject.link.match(idpattern);
			if (ids)
				subject.id = ids[1];
			$("link", entry).each(function() {
				if ($(this).attr("rel") == "image")
					subject.image = $(this).attr("href");
			});
			
			this.subjects.push(subject);
		}
		
		var author = $("author > name", entry).text();
		$("db\\:attribute", entry).each(function() {
			if ($(this).attr("name") == "comment" && $(this).text()) {
				if (author && author != "") {
					subject.comments.push('<span style="' + doubanbuddyInfo.getRandomSpanStyle() + '">[' + author + "]说: " + $(this).text()) + "</span>";
				} else {
					subject.comments.push($(this).text());
				}
			}
		});

	},
	
	
	findSubject: function(link, array) {
		if (!array)
			return null;
			
		for(var i = 0; i < array.length; ++i) {
			var subject = array[i];
			if (subject.link == link)
				return subject;
		}
		
		return null;
	},
	
	runHotness: function() {
		if (!this.subjects)
			return;
		
		var hots_count = 50; // define the const of hots to find
		var sortMethod = Application.prefs.getValue("extensions.doubanbuddy.rank", "pareto");
		var now = new Date().getTime() / 1000;
		
		if (sortMethod == "simple") {
			this.subjects.sort(function(a, b) { 
				return b.hot - a.hot;		
			});
			this.hots = this.subjects.slice(0, hots_count);
		} else if (sortMethod == "time") {
			this.subjects.sort(function(a, b) { 
				var f1 = b.hot - (now - b.published) / 28800;
				var f2 = a.hot - (now - a.published) / 28800;
				return  f1 - f2;
			});
			this.hots = this.subjects.slice(0, hots_count);
		} else if (sortMethod == "pareto" ) {
			this.subjects.sort(function(a, b) { 
				return b.hot - a.hot;		
			});

			if (this.subjects.length < hots_count * 1.5) {
				this.hots = this.subjects.slice(0, hots_count);			
			} else {
				var idset = {};
				this.hots = new Array();
				function paretoRandom(alpha) {
					var u = 1.0 - Math.random();
					return 1.0 / Math.pow(u, 1.0 / alpha);
				}
				for (var i = 0; i < hots_count; ++i) {
					for (var j = 0; j < 5; ++j) {
						var index = Math.round(paretoRandom(0.5)) - 1;
						if (!idset['' + index] && index >= 0 && index < this.subjects.length) {
							idset['' + index] = true;
							this.hots.push(this.subjects[index]);
							break;
						}					
					}
				}
				this.hots.sort(function(a, b) { 
					return b.hot - a.hot;		
				});
			}
		}
		
		for (var i = 0; i < this.hots.length; ++i) {
			// get subject description
			if (this.subjectInfo[this.hots[i].kind + this.hots[i].id]) {
				this.hots[i].description = this.subjectInfo[this.hots[i].kind + this.hots[i].id];
			} else {
				DoubanApi.getSubjectInfo(this.hots[i].link, this.hots[i].kind, this.hots[i].id, this.updateSubjectInfo);
			}
			
			// get rank
			var relEnabled = Application.prefs.getValue("extensions.doubanbuddy.relative", true);
			if (relEnabled) {
				var relitems = DoubanNGram.getNGramSim(this.hots[i].link);
				if (relitems) {
					for (var j = 0; j < relitems.length; ++j) {
						var subject = this.findSubject(relitems[j], this.subjects);
						if (subject) {
							var text = '<a href="' + subject.link + '">' + '<span style="' + this.getRandomSpanStyle() + '">[' + getKindClass(subject.kind) + "] " + subject.title + '</span></a>';
							this.hots[i].related.push(text);
						} else {
							subject = this.sayings[relitems[j]];
							if (subject) {
								this.hots[i].related.push("“" + subject + "”");		
							}
						}
					}
				}
			}
		}
		
		DoubanNGram.reset();
		Application.console.log("doubanbuddy: rank calculation done");
	},
	
	getRandomSpanStyle: function() {
		var colors = ["rgb(102, 102, 102)", "rgb(153, 153, 153)", "rgb(170, 170, 170)", "rgb(153, 51, 102)",
					  "rgb(102, 0, 0)", "rgb(153, 0, 153)", "rgb(102, 153, 0)", "rgb(153, 102, 51)", "rgb(153, 0, 102)",
					  "rgb(0, 51, 0)", "rgb(0, 153, 51)", "rgb(51, 153, 51)", "rgb(51, 51, 51)", "rgb(102, 102, 51)"];
		var style = "color: " + colors[Math.floor(Math.random() * colors.length)] + ";";
		return style;
	},
	
	updateSubjectInfo: function(data, link, kind, id) {
		var description = "";
		var image = "";
		if (kind == "book" || kind == "movie" || kind == "music" || kind == "review" || kind == "note"
				|| kind == "event") {
			var author = $("entry > author > name", data);
			if (author && $(author).text()) {
				description += $(author).text() + "<br /><br />";
			}
			var summary = $("entry > summary", data);
			if (summary && $(summary).text()) {
				var re= /<\S[^><]*>/g;				
				description += $(summary).text().replace(re, " ").substring(0, 80) + "......";
			}
			image = $("entry > author > link[rel=icon]", data).attr("href");
		} else if (kind == "contact" || kind == "friend") {
			var homepage = $("entry > link[rel=homepage]", data).attr("href");
			image = $("entry > link[rel=icon]", data).attr("href");
			if (homepage && homepage != "") {
				description += "<a href=\"" + homepage + "\">" + homepage + "</a><br /><br />";
			}
			
			var content = $("entry > content", data);
			if (content && $(content).text()) {
				var re= /<\S[^><]*>/g;				
				description += $(content).text().replace(re, " ").substring(0, 80) + "......";
			}
		}
		if (description != "") {
			doubanbuddyInfo.subjectInfo[kind + id] = description;
			var subject = doubanbuddyInfo.findSubject(link, doubanbuddyInfo.hots);
			if (subject) {
				subject.description = description;
				if (subject.image == "" && image && image != "")
					subject.image = image;
			}
		}
	},
	
	loadMiniblogData: function(data) {
		Application.console.log("doubanbuddy: call success - " + data);
		$("entry", data).each(function(){
			doubanbuddyInfo.extractSubject(this);
		});
		setTimeout("doubanbuddyInfo.crawlData()", 15 * 1000);
		if (doubanbuddyInfo.loop <= 0) {
			doubanbuddyInfo.runHotness();
			doubanbuddyInfo.status = "idle";
			doubanbuddyInfo.lastupdate = new Date();
			setTimeout("doubanbuddyInfo.startAnalysis()", 60 * 60 * 1000);
		}
	},
	
	error: function(data) {
		Application.console.log("doubanbuddy: call failed - " + data.status + " " + data.statusText + " " + data.responseText);
		doubanbuddyInfo.status = "idle";
		doubanbuddyInfo.lastupdate = new Date();
		setTimeout("doubanbuddyInfo.startAnalysis()", 60 * 60 * 1000);
	},
	
	openAboutDouban: function() {
		var newTab = gBrowser.addTab("about:douban");
    	gBrowser.selectedTab = newTab;
	}
	
}


var doubanbuddyInfo = new DoubanBuddyInfo();

function getKindClass(kind) {
	var kindMap = {
		"book" : "书籍",
		"movie" : "电影",
		"music" : "音乐",
		"tv" : "电视",
		"magazine" : "杂志",
		"group" : "小组",
		"place" : "地点",
		"photo" : "照片",
		"url" : "分享网页地址",
		"friend" : "好友",
		"contact" : "好友",
		"entry" : "博客文章",
		"event" : "同城活动",
		"online" : "线上活动",
		"blog" : "博客文章",
		"photo_album" : "相册",
		"recommendation" : "推荐",
		"topic" : "小组话题",
		"note" : "日记",
		"review" : "评论",
		"video" : "视频",
		"doulist" : "豆列",
		"site" : "博客",
		"artist_video" : "音乐人视频",
		"discussion" : "讨论"
	};
		
	if (kindMap[kind])
		return kindMap[kind];
		
	return kind;
}

// Parses an ISO 8601 formatted date string and returns a JavaScript Date object, returns null on parse error
// Example inputs:  "2004-06-17T18:00Z" "2004-06-17T18:34:12+02:00"
function iso8601ToJSDate(date_str) {
	var tmp = date_str.split("T");
	var date = tmp[0];

	date = date.split("-");
	var year = date[0];
	var month = date[1];
	var day = date[2];

	var hours = 0;
	var minutes = 0;
	var seconds = 0;
	var tz_mark = "Z";
	var tz_hours = 0;
	var tz_minutes = 0;
	var time, whole_time, tz;

	if(tmp.length == 2) {
		whole_time = tmp[1];
		tz_mark = whole_time.match("[Z+-]{1}");
		if(tz_mark) {
			tmp = whole_time.split(tz_mark);
			time = tmp[0];
			if(tz_mark == "+" || tz_mark == "-") {
				tz = tmp[1];
				tmp = tz.split(":");
				tz_hours = tmp[0];
				tz_minutes = tmp[1];
			}
		} else {
			tz_mark = "Z";
			time = whole_time;
		}
		tmp = time.split(":");
		hours = tmp[0];
		minutes = tmp[1];
		if(tmp.length == 3) {
			seconds = tmp[2];
		}
	}

	var utc = Date.UTC(year, month - 1, day, hours, minutes, seconds);
	var tmp_date;
	if(tz_mark == "Z") {
		tmp_date = new Date(utc);
	} else if(tz_mark == "+") {
		tmp_date = new Date(utc - tz_hours*3600000 - tz_minutes*60000);
	} else if(tz_mark == "-") {
		tmp_date = new Date(utc + tz_hours*3600000 + tz_minutes*60000);
	} else {
		tmp_date = "Invalid Date";
	}

	if (tmp_date == "Invalid Date") {
		return null;
	} else {
		return tmp_date;
	}
}